package com.jxtc.enterprise.tenant.controller;

import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.poi.excel.ExcelUtil;
import cn.hutool.poi.excel.ExcelWriter;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.jxtc.enterprise.common.aspectj.TokenRequired;
import com.jxtc.enterprise.common.constants.MealTypeEnum;
import com.jxtc.enterprise.common.constants.OrderPaymentStatus;
import com.jxtc.enterprise.common.entity.DeliveryLocker;
import com.jxtc.enterprise.common.entity.OrderDetail;
import com.jxtc.enterprise.common.entity.OrderPayment;
import com.jxtc.enterprise.common.mapper.DeliverLockerMapper;
import com.jxtc.enterprise.common.mapper.OrderDetailMapper;
import com.jxtc.enterprise.common.mapper.OrderMapper;
import com.jxtc.enterprise.common.service.DeliveryLockerOpenApiService;
import com.jxtc.enterprise.common.utils.Result;
import com.jxtc.enterprise.common.utils.ResultGenerator;
import com.jxtc.enterprise.common.vo.DeliverBoxApiParamVO;
import com.jxtc.enterprise.common.vo.DeliveryLockerOpenDoorParameterVO;
import com.jxtc.enterprise.common.vo.DeliveryLockerOpenDoorVO;
import com.jxtc.enterprise.common.vo.PaginationResponseVO;
import com.jxtc.enterprise.merchant.dto.order_manage.StoreInfoForOrderDTO;
import com.jxtc.enterprise.tenant.dto.*;
import com.jxtc.enterprise.tenant.vo.TenantOrderDetailVO;
import com.jxtc.enterprise.tenant.vo.TenantOrderVO;
import com.jxtc.enterprise.tenant.vo.orderMange.req.DeliveryLockerBoxReqVo;
import com.jxtc.enterprise.tenant.vo.orderMange.resp.DeliveryLockerCabAndBoxInfoRespVo;
import com.jxtc.enterprise.tenant.vo.orderMange.resp.DeliveryLockerCabRespVo;
import com.jxtc.enterprise.tenant.vo.orderMange.resp.WaitIntervalForBatchOpenMultiDoorRespVo;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Sheet;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.text.MessageFormat;
import java.time.LocalDate;
import java.util.*;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * 管理员端，订单管理页面 的接口
 */
@RestController
@RequestMapping("/jxtc/enterprise/tenant/orderManage")
@RequiredArgsConstructor
@Slf4j
public class TenantOrderManageController {

    private final OrderMapper orderMapper;
    private final OrderDetailMapper orderDetailMapper;
    private final DeliverLockerMapper deliverLockerMapper;
    private final DeliveryLockerOpenApiService deliveryLockerOpenApiService;

    /**
     * 当一次打开一个柜门时，调用开柜门接口后，等待的间隔时间列表，以 "," 分割的字符串
     */
    @Value("${locker.waitIntervalInMsList}")
    private String waitIntervalInMsList = "500,1000,3000,5000";

    /**
     * 默认选中等待的间隔时间，的配合 waitIntervalInMsList 一起使用，必选是 waitIntervalInMsList 中的某一项
     */
    @Value("${locker.defaultSelectedWaitIntervalInMs}")
    private Integer defaultSelectedWaitIntervalInMs = 3000;


    @Operation(summary = "2.6.1 租户根据查询条件(公司名称，查询时间)分页查询订单", tags = {"2.6 租户订单管理"}, operationId = "2.6.1")
    @TokenRequired(role = TokenRequired.ROLE_TENANT)
    @PostMapping("/queryOrderByPage")
    Result<?> queryOrderByPage(@RequestBody TenantPageOrderForTenantDTO dto, HttpServletRequest request) {
        String tenantId = (String) request.getAttribute("userId");
        log.info("/queryOrderByPage, tenantId: {}, request:{}", tenantId, JSONObject.toJSONString(dto));

        PaginationResponseVO<TenantOrderVO> pageData = pageOrderByCompanyNameAndTime(tenantId, dto.getKeyword(),
                dto.getStartDateTime(), dto.getEndDateTime(), dto.getCurrent(), dto.getSize());

        log.info("/queryOrderByPage, response: {}", JSONObject.toJSONString(pageData));
        return ResultGenerator.genSuccessResult(pageData);
    }


    @ApiResponse(responseCode = "200", description = "code 为 200 时，data 字段为包含订单详情列表的分页信息")
    @Operation(summary = "2.6.2 租户根据公司 ID 和日期查询订单详情列表", tags = {"2.6 租户订单管理"}, operationId = "2.6.2")
    @TokenRequired(role = TokenRequired.ROLE_TENANT)
    @PostMapping("/queryOrderDetail")
    public Result<PaginationResponseVO<TenantOrderDetailVO>> queryOrderDetail(@RequestBody TenantQueryOrderDetailDTO dto) {
        log.info("/queryOrderDetail, request: {}", JSONObject.toJSONString(dto));
        PaginationResponseVO<TenantOrderDetailVO> vo = pageOrderDetailByCompanyIdAndDate(dto.getCompanyId(), dto.getDate(), dto.getPage(), dto.getSize());

        log.info("/queryOrderDetail, response: {}", JSONObject.toJSONString(vo));
        return ResultGenerator.genSuccessResult(vo);
    }


    @ApiResponse(responseCode = "200", description = "导出成功")
    @Operation(summary = "2.6.3 租户根据公司和日期信息列表将订单信息导出成 Excel", tags = {"2.6 租户订单管理"}, operationId = "2.6.3")
    @TokenRequired(role = TokenRequired.ROLE_TENANT)
    @PostMapping("/export2Excel")
    public void export2Excel(HttpServletResponse response, @Valid @RequestBody TenantOrderExportDTO dto, HttpServletRequest request) {
        String tenantId = (String) request.getAttribute("userId");
        log.info("/export2Excel, tenantId: {}, request: {}", tenantId, JSONObject.toJSONString(dto));

        List<TenantCompanyWithDate> rows = dto.getRows();

        if (dto.getSelectAll() != null && dto.getSelectAll()) {
            String companyName = dto.getKeyword() == null ? "" : dto.getKeyword();
            PaginationResponseVO<TenantOrderVO> vo = pageOrderByCompanyNameAndTime(tenantId, companyName, null, null, 1, Integer.MAX_VALUE);

            rows = vo.getRecords().stream()
                    .map(record -> new TenantCompanyWithDate(record.getCompanyId(), record.getCompanyName(), record.getDate()))
                    .collect(Collectors.toList());
        }

        try (ExcelWriter writer = ExcelUtil.getWriter(true)) {
            boolean firstSheet = true;
            List<TenantOrderDetailVO> summaryOrderDetailList = new ArrayList<>();
            for (TenantCompanyWithDate row : rows) {
                PaginationResponseVO<TenantOrderDetailVO> orderDetailVO = pageOrderDetailByCompanyIdAndDate(row.getCompanyId(), row.getDate(), 1, Integer.MAX_VALUE);
                List<Map<String, Object>> data = new ArrayList<>();
                summaryOrderDetailList.addAll(orderDetailVO.getRecords());

                // 【1103新增功能】 先某个公司的员工的所有订单的id，查询出该公司员工订单的 店铺相关信息，用来作为导出excel文件中，新增的【店铺名】，【支付给商家的费用】 两列的值
                Map<String, StoreInfoForOrderDTO> orderIdToVoMap = evalOrderIdToMerchantInfoMap(orderDetailVO.getRecords());

                int idx = 1;
                for (TenantOrderDetailVO orderDetail : orderDetailVO.getRecords()) {
                    // 【1103新增功能】 从map中取出各个订单支付信息中，需要为店铺支付的费用相关的DTO
                    StoreInfoForOrderDTO storeInfoForOrderDto = orderIdToVoMap.getOrDefault(orderDetail.getOrderId(), new StoreInfoForOrderDTO());
                    data.add(createRow(idx++, orderDetail.getUsername(), orderDetail.getDepartment(), orderDetail.getOrderName(), orderDetail.getPrice(),
                            storeInfoForOrderDto.getStoreName(), storeInfoForOrderDto.getPayForMerchant(), orderDetail.getOrderTime(), orderDetail.getNote()));
                }

                // 将前端传递过来的 '2024-07-22 (午餐 15:00 ~ 14:00)' 格式的时间，分离出日期
                String[] dateAndMealFlag = row.getDate().split(" ");
                if (dateAndMealFlag.length < 2) {
                    log.error("companyId: {}, rawDate: {}", row.getCompanyId(), row.getDate());
                    throw new IllegalArgumentException("时间格式不正确，请联系管理员排查");
                }
                String date = dateAndMealFlag[0];
                String meal = dateAndMealFlag[1].replace("(", "");
                // 将前端传递过来的 '2024-07-22 (午餐 15:00 ~ 14:00)' 解析为类似 15001400 格式，拼接在 sheet 名称后
                // 仅靠 date 和 meal 无法避免以下格式的数据导致 sheet 页名称重复问题
                // 2024-07-31 (午餐 19:00 ~ 10:00)
                // 2024-07-31 (午餐 19:00 ~ 10:30)
                // 避免类似这种数据导致导出的 excel 中缺少 sheet 页
                // 注：excel 中 sheet 页名称中不能包含 ":"
                String timeRange = parseTimeRange(row.getDate());

                String sheetName = row.getCompanyName() + "-" + date + meal + timeRange;
                addSheet(writer, sheetName, data, firstSheet);
                firstSheet = false;
            }
            // 将所有 sheet 页的数据汇总到一个 sheet 页中，并按下单时间降序排列
            addSummarySheet(summaryOrderDetailList, writer);

            String filename = System.currentTimeMillis() + ".xlsx";

            response.setContentType("application/vnd.ms-excel;charset=utf-8");
            response.setHeader("Content-Disposition", "attachment;filename=" + filename);
            writer.flush(response.getOutputStream(), true);
        } catch (Exception e) {
            e.printStackTrace();
            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            throw new RuntimeException("Error while generating Excel file", e);
        }
    }

    private PaginationResponseVO<TenantOrderVO> pageOrderByCompanyNameAndTime(String tenantId, String keyword,
                                                                              String startDateTime, String endDateTime,
                                                                              int current, int size) {
        Page<TenantOrderStatisticDTO> page = new Page<>(current, size);

        // 如果开始时间为空，则设置为最早日期
        if (startDateTime == null || startDateTime.isEmpty()) {
            startDateTime = "2024-01-01";
        }
        LocalDate startDate = LocalDateTimeUtil.parseDate(startDateTime);
        // 如果结束时间为空，则设置为当前日期的后一天；如果结束时间不为空，则将 yyyy-MM-dd 格式的时间转换成 LocalDate 类型
        LocalDate today = LocalDate.now();
        LocalDate endDate = today.plusDays(1);
        if (endDateTime != null && !endDateTime.isEmpty()) {
            endDate = LocalDateTimeUtil.parseDate(endDateTime);
        }

        Page<TenantOrderStatisticDTO> result = orderMapper.queryOrderStatisticForTenant(page, tenantId, keyword, startDate, endDate);
        List<TenantOrderVO> records = result.getRecords().stream()
                .map(TenantOrderVO::new)
                .collect(Collectors.toList());

        return new PaginationResponseVO<>(result.getTotal(), result.getCurrent(), records);
    }

    /**
     * 将类似 '2024-07-22 (午餐 15:00 ~ 14:00)' 格式的数据解析为 15001400 这种格式的字符串
     */
    private String parseTimeRange(String input) {
        // 定义匹配时间的正则表达式
        Pattern pattern = Pattern.compile("(\\d{2}:\\d{2}) ~ (\\d{2}:\\d{2})");
        Matcher matcher = pattern.matcher(input);

        if (matcher.find()) {
            // 提取开始时间和结束时间
            String startTime = matcher.group(1).replace(":", "");
            String endTime = matcher.group(2).replace(":", "");
            // 合并时间部分
            return startTime + endTime;
        }
        return "";
    }

    private void addSummarySheet(List<TenantOrderDetailVO> summaryOrderDetailList, ExcelWriter writer) {
        // 按照下单时间降序排序
        List<TenantOrderDetailVO> sortedByTime = summaryOrderDetailList.stream()
                .sorted(Comparator.comparing(TenantOrderDetailVO::getOrderTime, Comparator.reverseOrder()))
                .collect(Collectors.toList());
        // 【1103新增功能】 先某个公司的员工的所有订单的id，查询出该公司员工订单的 店铺相关信息，用来作为导出excel文件中，新增的【店铺名】，【支付给商家的费用】 两列的值
        Map<String, StoreInfoForOrderDTO> orderIdToVoMap = evalOrderIdToMerchantInfoMap(summaryOrderDetailList);
        List<Map<String, Object>> data = new ArrayList<>();
        int idx = 1;
        for (TenantOrderDetailVO orderDetail : sortedByTime) {
            // 【1103新增功能】 从map中取出各个订单支付信息中，需要为店铺支付的费用相关的DTO
            StoreInfoForOrderDTO storeInfoForOrderDto = orderIdToVoMap.getOrDefault(orderDetail.getOrderId(), new StoreInfoForOrderDTO());
            data.add(createRow(idx++, orderDetail.getUsername(), orderDetail.getDepartment(), orderDetail.getOrderName(),
                    orderDetail.getPrice(),
                    storeInfoForOrderDto.getStoreName(), storeInfoForOrderDto.getPayForMerchant(),
                    orderDetail.getOrderTime(), orderDetail.getNote()));
        }
        // 添加一个新的 sheet 页，并设置为第一个 sheet 页
        Sheet summarySheet = writer.getWorkbook().createSheet("汇总");
        writer.getWorkbook().setSheetOrder("汇总", 0);

        // 设置“餐品”这一列的宽度，索引从 0 开始
        int dishNameColumnIndex = 3;
        summarySheet.setColumnWidth(dishNameColumnIndex, 40 * 256);

        // 设置“下单时间”这一列的宽度
        int orderTimeColumnIndex = 5;
        summarySheet.setColumnWidth(orderTimeColumnIndex, 20 * 256);

        // 写入数据
        writer.setSheet("汇总");
        writer.write(data, true);

        // 默认打开 “汇总” 这个 sheet 页
        writer.getWorkbook().setActiveSheet(0);
    }

    private PaginationResponseVO<TenantOrderDetailVO> pageOrderDetailByCompanyIdAndDate(String companyId, String rawDate, int current, int size) {
        Page<TenantOrderDetailDTO> page = new Page<>(current, size);
        // 将前端传递过来的 '2024-07-22 (午餐 15:00 ~ 14:00)' 格式的时间，分离出日期
        String[] dateAndMealFlag = rawDate.split(" \\(");
        if (dateAndMealFlag.length != 2) {
            log.error("companyId: {}, rawDate: {}, current: {}, size: {}", companyId, rawDate, current, size);
            throw new IllegalArgumentException("时间格式不正确，请联系管理员排查");
        }
        String date = dateAndMealFlag[0];
        String meal = dateAndMealFlag[1].replace(")", "");
        LocalDate localDate = LocalDateTimeUtil.parseDate(date);
        Page<TenantOrderDetailDTO> result = orderMapper.queryOrderDetailForTenant(page, companyId, localDate, meal);

        // order_payment 表中没有 订单备注，此处存在问题，订单的备注信息应该数据订单，而不是 具体的某个菜品；此处从 order_detail 表中查询出某个订单的备注信息
        Map<String, String> orderIdToItemMap = evalOrderNoteForOrders(result.getRecords());

        // TODO: 订单名称规则？
        List<TenantOrderDetailVO> records = result.getRecords().stream().map(detail -> {
            TenantOrderDetailVO vo = new TenantOrderDetailVO(detail);
            String orderId = detail.getOrderId(); // order_payment表中的id，order_detail 表中的 order_id
            // 每个订单可能包含多份餐品，根据order_payment表中的id，查询出该订单包含的多份餐品，foodDetailList 为订单中的多份餐品
            List<TenantOrderDetailStoreFoodDTO> foodDetailList = orderDetailMapper.selectByOrderIdForTenant(orderId);
            String orderName = foodDetailList.stream().map(TenantOrderDetailStoreFoodDTO::getFoodName).collect(Collectors.joining(","));
            vo.setOrderName(orderName);
            // 为导出的数据补充 订单备注 note
            if (orderIdToItemMap.containsKey(orderId)) {
                vo.setNote(orderIdToItemMap.get(orderId));
            }

            String deliveryLockerBoxNo = foodDetailList.stream()
                    // 查询到的订单详情中，过滤掉没有外卖柜信息的数据
                    .filter(dto -> StringUtils.hasText(dto.getLockerInfo()))
                    .map(TenantOrderDetailStoreFoodDTO::getLockerBoxNo)
                    // 将多个外卖柜货柜编号按照 "," 拼接
                    .collect(Collectors.joining(","));
            vo.setDeliveryLockerBoxNo(deliveryLockerBoxNo);
            return vo;
        }).collect(Collectors.toList());
        return new PaginationResponseVO<>(result.getTotal(), result.getCurrent(), records);
    }

    private void addSheet(ExcelWriter writer, String sheetName, List<Map<String, Object>> data, boolean firstSheet) {
        if (firstSheet) {
            writer.renameSheet(sheetName);
        } else {
            writer.setSheet(sheetName);
        }
        writer.write(data, true);

        // 获取当前工作表
        Sheet sheet = writer.getSheet();

        // 设置“餐品”这一列的宽度，索引从 0 开始
        int dishNameColumnIndex = 3;
        sheet.setColumnWidth(dishNameColumnIndex, 40 * 256);

        // 设置“下单时间”这一列的宽度
        int orderTimeColumnIndex = 5;
        sheet.setColumnWidth(orderTimeColumnIndex, 20 * 256);
    }

    private Map<String, Object> createRow(Integer index, String username, String department, String dish, Double price, String storeName, Integer priceForMerchantInt, String orderTime, String note) {
        // 使用 LinkedHashMap 而不是 HashMap 防止标题栏乱序
        Map<String, Object> row = new LinkedHashMap<>();
        row.put("序号", index);
        row.put("用户名", username);
        row.put("部门", department);
        row.put("餐品", dish);
        row.put("给用户的销售价格", price);
        row.put("店铺", storeName);
        Double priceForMerchantDouble = priceForMerchantInt / 100.0;
        row.put("支付给店铺的费用", priceForMerchantDouble);
        row.put("下单时间", orderTime);
        row.put("备注", note);
        return row;
    }

    /**
     * 【1103新增功能】 查询出各个订单支付信息中，需要给商户结算相关的信息
     */
    private Map<String, StoreInfoForOrderDTO> evalOrderIdToMerchantInfoMap(List<TenantOrderDetailVO> orderDetailList) {

        List<String> orderIds = orderDetailList.stream().map(TenantOrderDetailVO::getOrderId).collect(Collectors.toList());
        // 【1103新增功能】 先某个公司的员工的所有订单的id，查询出该公司员工订单的 店铺相关信息，用来作为导出excel文件中，新增的【店铺名】，【支付给商家的费用】 两列的值
        Map<String, StoreInfoForOrderDTO> orderIdToVoMap = new HashMap<>();
        if (!CollectionUtils.isEmpty(orderIds)) {
            List<StoreInfoForOrderDTO> storeInfoForOrderDTOS = orderMapper.queryOrderStoreInfoByOrderIds(orderIds);
            // 将查询出的结果按照订单id进行聚合，key为订单id，value为订单中菜品来自那些店铺名称，需要给店铺支付的总金额；
            // todo 此处存在一个问题，当某个订单来自多个店铺时，sql查询结果对应的单个DTO中，店铺名称是多个店铺，而需要支付给商家的金额累加在一起，无法区分该订单应该为各个商家支付多少费用
            orderIdToVoMap = storeInfoForOrderDTOS.stream().collect((Collectors.toMap(StoreInfoForOrderDTO::getOrderId, Function.identity())));
        }

        return orderIdToVoMap;
    }

    private Map<String, String> evalOrderNoteForOrders(List<TenantOrderDetailDTO> records) {

        List<String> orderIds = records.stream().map(TenantOrderDetailDTO::getOrderId).distinct().collect(Collectors.toList());
        if (CollectionUtils.isEmpty(orderIds)) {
            return new HashMap<>();
        }
        List<OrderDetail> orderDetails = orderDetailMapper
                .selectList(new LambdaQueryWrapper<OrderDetail>()
                        .in(OrderDetail::getOrderId, orderIds));
        // 一个orderId在 order_detail 表中可能会对应多条记录，因为一份订单可能会有多份菜品，order_detail中多条记录的note都会有值，此处聚合成map时，当一个orderId有多条记录时，取其中一个note不为空的记录的note字段的值作为map的value
        // 理论上，order_detail表中，orderId相同的多条记录的note字段的值应该是完全一样的，或者都为null 或 空字符串，或者都有值
        Map<String, String> orderIdToNoteMap =
                orderDetails.stream().collect(
                        Collectors.toMap(
                                OrderDetail::getOrderId,
                                OrderDetail::getNote,
                                (order1Note, order2Note) -> StringUtils.hasText(order1Note) ? order1Note : order2Note)
                );

        return orderIdToNoteMap;
    }


    /**
     * 2.6.4 查询某个公司所属的外卖柜列表
     */
    @TokenRequired(role = TokenRequired.ROLE_TENANT)
    @GetMapping("/queryDeliveryLockerCabList")
    public Result<List<DeliveryLockerCabRespVo>> queryDeliveryLockerCabList(@RequestParam(value = "companyId") String companyId) {
        log.info("/queryDeliveryLockerCabList, companyId: {}", companyId);
        LambdaQueryWrapper<DeliveryLocker> deliveryLockerQueryWrapper = new LambdaQueryWrapper<DeliveryLocker>()
                .eq(DeliveryLocker::getCompanyId, companyId)
                .eq(DeliveryLocker::getDelFlag, false)
                // 根据货柜编码升序排列
                .orderByAsc(DeliveryLocker::getShortName);
        List<DeliveryLocker> deliveryLockers = deliverLockerMapper.selectList(deliveryLockerQueryWrapper);
        List<DeliveryLockerCabRespVo> voList = deliveryLockers.stream()
                .map(this::buildDeliveryLockerCabRespVo)
                .collect(Collectors.toList());
        log.info("/queryDeliveryLockerCabList, response: {}", JSONObject.toJSONString(voList));
        return ResultGenerator.genSuccessResult(voList);
    }

    private DeliveryLockerCabRespVo buildDeliveryLockerCabRespVo(DeliveryLocker deliveryLocker) {
        DeliveryLockerCabRespVo vo = new DeliveryLockerCabRespVo();
        vo.setDeliveryLockerId(deliveryLocker.getId());
        vo.setDeliveryLockerName(deliveryLocker.getName());
        vo.setDeliveryLockerShortName(deliveryLocker.getShortName());
        vo.setDeliveryLockerCab(deliveryLocker.getCab());
        return vo;
    }


    /**
     * 2.6.5 通过外卖柜 ID 打开该外卖柜所有的窗口
     *
     * @param deliveryLockerId 外卖柜 ID
     */
    @TokenRequired(role = TokenRequired.ROLE_TENANT)
    @GetMapping("/openAllDeliveryLockerBoxById")
    public Result<Void> openAllDeliveryLockerBoxById(@RequestParam(value = "deliveryLockerId") String deliveryLockerId,
                                                     @RequestParam(value = "waitIntervalInMs", defaultValue = "3000") Integer waitIntervalInMs) {
        log.info("/openAllDeliveryLockerBoxById, deliveryLockerId: {}, waitIntervalInMs: {}", deliveryLockerId, waitIntervalInMs);
        DeliveryLocker deliveryLocker = deliverLockerMapper.selectById(deliveryLockerId);
        if (deliveryLocker == null) {
            log.warn("/openAllDeliveryLockerBoxById, 外卖柜不存在, deliveryLockerId: {}", deliveryLockerId);
            return ResultGenerator.genFailResult("打开失败，ID 为 " + deliveryLockerId + " 的外卖柜不存在");
        }

        String clientNo = deliveryLocker.getClientNo();
        // 使用 UUID 生成一个 32 位的字符串作为 orderNo
        String orderNo = UUID.randomUUID().toString().replace("-", "");
        String appId = deliveryLocker.getAppId();
        String appKey = deliveryLocker.getAppKey();
        Integer cab = deliveryLocker.getCab();

        int groupSize = 5;

        Integer colStart = deliveryLocker.getColStart();
        Integer colEnd = deliveryLocker.getColEnd();
        List<DeliveryLockerOpenDoorParameterVO> parameterList = new ArrayList<>();
        for (int col = colStart; col <= colEnd; col++) {
            DeliveryLockerOpenDoorParameterVO parameter = new DeliveryLockerOpenDoorParameterVO();
            // 计算这一组的开始窗口号
            int batchColStart = ((col - 1) / groupSize) * groupSize + 1;
            // 计算这一组的结束窗口号
            int batchColEnd = Math.min(batchColStart + groupSize - 1, colEnd);
            // 每五个一组，更换一次 orderNo
            if ((col - 1) % groupSize == 0) {
                orderNo = UUID.randomUUID().toString().replace("-", "");
            }
            // 打开某个柜子的所有柜门时，每次打开5个，使用同一个虚拟orderNo，orderNo格式为  shortName_label_startCol_endCol ，orderNo 编码了 柜子简称，柜子 贴标，本次开柜门接口调用，打开 那几个窗格
            String messageForOrderNo = MessageFormat.format("{0}_{1}_{2}_{3}", deliveryLocker.getShortName(), deliveryLocker.getLockerLabel(), batchColStart, batchColEnd);
            parameter.setOrderNo(orderNo);
            parameter.setCab(String.valueOf(cab));
            String sn = String.valueOf(col % 5);
            parameter.setSn(sn);
            parameter.setCol(String.valueOf(col));
            parameter.setPrice("0.00");
            parameter.setNo(MessageFormat.format("{0}_{1}", deliveryLocker.getLockerLabel(), col));
            parameter.setName("");
            parameter.setType("5");
            parameterList.add(parameter);
            log.info("/openAllDeliveryLockerBoxById, orderNo: {}, messageForOrderNo: {}, cab: {}, col: {}, sn: {}", orderNo, messageForOrderNo, cab, col, sn);
        }

        deliveryLockerOpenApiService.openDoorListMoreThanFive(clientNo, appId, appKey, parameterList, waitIntervalInMs);
        return ResultGenerator.genSuccessResult();
    }


    /**
     * 2.6.6 打开订单所对应的所有外卖柜的窗口
     */
    @TokenRequired(role = TokenRequired.ROLE_TENANT)
    @PostMapping("/openAllDeliveryLockerBoxForOrder")
    public Result<Void> openAllDeliveryLockerBoxForOrder(@Valid @RequestBody DeliveryLockerBoxReqVo reqVo) {

        String requestId = UUID.randomUUID().toString().replace("-", "");

        log.info("{} /openAllDeliveryLockerBoxForOrder, request: {}", requestId, JSONObject.toJSONString(reqVo));
        // 前端传递过来的日期字段，实际上拼接了用餐时段，形如：2025-06-29 (午餐)
        // 所以需要分割出正确的订单日期
        String date = reqVo.getDate();
        String[] dateFlagAndMealType = date.split(" ");
        String dateFlag = date;
        if (dateFlagAndMealType.length == 2) {
            dateFlag = dateFlagAndMealType[0];
        }

        LambdaQueryWrapper<OrderPayment> orderPaymentWrapper = new LambdaQueryWrapper<OrderPayment>()
                .eq(OrderPayment::getCompanyId, reqVo.getCompanyId())
                .eq(OrderPayment::getDateFlag, dateFlag)
                // 仅查询订单状态为 2, 3, 8 的订单
                .in(OrderPayment::getStatus, OrderPaymentStatus.TO_SEND.getValue(), OrderPaymentStatus.TO_RECEIVE.getValue(), OrderPaymentStatus.COMPLETED.getValue())
                .select(OrderPayment::getId);
        List<String> orderIdList = orderMapper.selectObjs(orderPaymentWrapper);
        if (CollectionUtils.isEmpty(orderIdList)) {
            log.warn("/openAllDeliveryLockerBoxForOrder, 没有符合条件的订单, companyId: {}, dateFlag: {}", reqVo.getCompanyId(), dateFlag);
            return ResultGenerator.genFailResult("打开柜门失败，该公司今日没有符合条件的订单");
        }

        String mealTypeCn = reqVo.getMealType();
        String mealTypeEn = Arrays.stream(MealTypeEnum.values())
                .filter(mealTypeEnum -> mealTypeEnum.getMealTypeCn().equals(mealTypeCn))
                .map(MealTypeEnum::getMealTypeEn)
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("用餐时段不合法: " + mealTypeCn));

        LambdaQueryWrapper<OrderDetail> orderDetailQueryWrapper = new LambdaQueryWrapper<OrderDetail>()
                .in(OrderDetail::getOrderId, orderIdList)
                .eq(OrderDetail::getCompanyId, reqVo.getCompanyId())
                .eq(OrderDetail::getMealType, mealTypeEn)
                .select(OrderDetail::getId, OrderDetail::getLockerInfo, OrderDetail::getLockerBoxNo)
                // 根据柜门编号升序排列，以保证打开柜门时是按顺序的
                .orderByAsc(OrderDetail::getLockerBoxNo);
        List<OrderDetail> orderDetailList = orderDetailMapper.selectList(orderDetailQueryWrapper);
        if (CollectionUtils.isEmpty(orderDetailList)) {
            log.warn("/openAllDeliveryLockerBoxForOrder, 没有符合条件的订单明细, companyId: {}, dateFlag: {}, mealTypeCn:{}, mealTypeEn: {}, orderIdList: {}",
                    reqVo.getCompanyId(), dateFlag, mealTypeCn, mealTypeEn, orderIdList);
            return ResultGenerator.genFailResult("打开柜门失败，该公司今日没有符合条件的订单");
        }

        // 找出包含 locker_info 的订单详情信息
        List<OrderDetail> orderNeedToOpenDoorList = orderDetailList.stream()
                .filter(orderDetail -> StringUtils.hasText(orderDetail.getLockerInfo()))
                .collect(Collectors.toList());
        log.info("/openAllDeliveryLockerBoxForOrder, {} 个订单详情需要打开柜门", orderNeedToOpenDoorList.size());

        List<DeliveryLockerOpenDoorVO> openDoorArgsList = new ArrayList<>(orderNeedToOpenDoorList.size());
        for (OrderDetail orderDetail : orderNeedToOpenDoorList) {
            String lockerInfo = orderDetail.getLockerInfo();
            DeliverBoxApiParamVO boxApiParamVO = JSONObject.parseObject(lockerInfo, DeliverBoxApiParamVO.class);

            DeliveryLockerOpenDoorParameterVO openDoorVo = new DeliveryLockerOpenDoorParameterVO();
            // 这里直接使用订单详情 ID 作为 orderNo，每笔订单详情 ID 一定是不重复的
            openDoorVo.setOrderNo(requestId);
            Integer cab = boxApiParamVO.getCab();
            openDoorVo.setCab(String.valueOf(cab));
            // TODO: 验证 sn 一直为 1 是否可以正常批量打开柜门
            openDoorVo.setSn(orderDetail.getLockerBoxNo());
            Integer col = boxApiParamVO.getCol();
            openDoorVo.setCol(String.valueOf(col));
            openDoorVo.setPrice("0.00");
            openDoorVo.setNo(orderDetail.getId());
            openDoorVo.setName("");
            openDoorVo.setType("5");

            DeliveryLockerOpenDoorVO openDoorArgs = new DeliveryLockerOpenDoorVO();
            openDoorArgs.setClientNo(boxApiParamVO.getClientNo());
            openDoorArgs.setAppId(boxApiParamVO.getAppId());
            openDoorArgs.setAppKey(boxApiParamVO.getAppKey());
            openDoorArgs.setOrderNo(requestId);
            openDoorArgs.setParameter(openDoorVo);

            openDoorArgsList.add(openDoorArgs);
        }

        // 根据 clientNo + appId + appKey 分组，以便于连接相同外卖柜控制终端的外卖柜窗口，可以调用批量开柜门接口，减少调用接口次数和等待时间
        Map<String, List<DeliveryLockerOpenDoorVO>> openDoorArgsGroupByClient = openDoorArgsList.stream()
                .collect(Collectors.groupingBy(args -> args.getClientNo() + args.getAppId() + args.getAppKey()));
        int callOpenDoorApiCount = 0;
        for (List<DeliveryLockerOpenDoorVO> openDoorArgsListInGroup : openDoorArgsGroupByClient.values()) {
            DeliveryLockerOpenDoorVO commonArgs = openDoorArgsListInGroup.get(0);
            String clientNo = commonArgs.getClientNo();
            String appId = commonArgs.getAppId();
            String appKey = commonArgs.getAppKey();
            List<DeliveryLockerOpenDoorParameterVO> parameter = openDoorArgsListInGroup.stream()
                    .map(DeliveryLockerOpenDoorVO::getParameter)
                    .collect(Collectors.toList());

            // 调用批量打开柜门接口
            log.info("/openAllDeliveryLockerBoxForOrder, 对于 clientNo: {}, appId: {}, appKey: {} 的外卖柜终端，有 {} 个窗口需要打开", clientNo, appId, appKey, parameter.size());
            deliveryLockerOpenApiService.openDoorListMoreThanFive(clientNo, appId, appKey, parameter, reqVo.getWaitIntervalInMs());
            callOpenDoorApiCount++;
        }


        log.info("/openAllDeliveryLockerBoxForOrder, 成功调用批量打开柜门接口 {} 次", callOpenDoorApiCount);

        return ResultGenerator.genSuccessResult();
    }


    /**
     * 2.6.7 获取货柜编号及窗户编号下拉列表
     *
     * @param companyId 公司 ID
     */
    @TokenRequired(role = TokenRequired.ROLE_TENANT)
    @GetMapping("/queryDeliveryCabAndBoxInfoList")
    public Result<List<DeliveryLockerCabAndBoxInfoRespVo>> queryDeliveryCabAndBoxInfoList(@RequestParam(value = "companyId") String companyId) {
        log.info("/queryDeliveryCabAndBoxInfoList, companyId: {}", companyId);
        LambdaQueryWrapper<DeliveryLocker> deliveryLockerQueryWrapper = new LambdaQueryWrapper<DeliveryLocker>()
                .eq(DeliveryLocker::getCompanyId, companyId)
                .eq(DeliveryLocker::getDelFlag, false)
                // 根据货柜编码升序排列
                .orderByAsc(DeliveryLocker::getShortName);
        List<DeliveryLocker> deliveryLockerList = deliverLockerMapper.selectList(deliveryLockerQueryWrapper);
        List<DeliveryLockerCabAndBoxInfoRespVo> voList = deliveryLockerList.stream()
                .map(this::buildDeliveryLockerCabAndBoxInfoRespVo)
                .collect(Collectors.toList());
        log.info("/queryDeliveryCabAndBoxInfoList, response: {}", JSONObject.toJSONString(voList));
        return ResultGenerator.genSuccessResult(voList);
    }

    private DeliveryLockerCabAndBoxInfoRespVo buildDeliveryLockerCabAndBoxInfoRespVo(DeliveryLocker deliveryLocker) {
        DeliveryLockerCabAndBoxInfoRespVo vo = new DeliveryLockerCabAndBoxInfoRespVo();
        vo.setDeliveryLockerId(deliveryLocker.getId());
        vo.setDeliveryLockerName(deliveryLocker.getName());
        vo.setDeliveryLockerShortName(deliveryLocker.getShortName());
        vo.setDeliveryLockerCab(deliveryLocker.getCab());

        Integer colStart = deliveryLocker.getColStart();
        Integer colEnd = deliveryLocker.getColEnd();
        vo.setDeliveryLockerColStart(colStart);
        vo.setDeliveryLockerColEnd(colEnd);

        List<Integer> colList = new ArrayList<>(colEnd - colStart + 1);
        for (int i = colStart; i <= deliveryLocker.getColEnd(); i++) {
            colList.add(i);
        }
        vo.setDeliveryLockerColList(colList);
        return vo;
    }


    /**
     * 2.6.8 打开指定外卖柜的某个窗口
     *
     * @param deliveryLockerId 外卖柜 ID
     * @param col              窗口编号
     */
    @TokenRequired(role = TokenRequired.ROLE_TENANT)
    @GetMapping("/openDeliveryLockerBoxByIdAndCol")
    public Result<Void> openDeliveryLockerBoxByIdAndCol(@RequestParam(value = "deliveryLockerId") String deliveryLockerId, @RequestParam(value = "col") Integer col) {
        log.info("/openDeliveryLockerBoxByIdAndCol, deliveryLockerId: {}, col: {}", deliveryLockerId, col);
        DeliveryLocker deliveryLocker = deliverLockerMapper.selectById(deliveryLockerId);
        if (deliveryLocker == null) {
            log.warn("/openDeliveryLockerBoxByIdAndCol, 未找到对应的外卖柜, deliveryLockerId: {}", deliveryLockerId);
            return ResultGenerator.genFailResult("ID 为 " + deliveryLockerId + " 的外卖柜不存在");
        }

        String clientNo = deliveryLocker.getClientNo();
        // 使用 UUID 生成一个 32 位的字符串作为 orderNo
        String orderNo = UUID.randomUUID().toString().replace("-", "");
        String appId = deliveryLocker.getAppId();
        String appKey = deliveryLocker.getAppKey();
        Integer cab = deliveryLocker.getCab();

        DeliveryLockerOpenDoorParameterVO openDoorVo = new DeliveryLockerOpenDoorParameterVO();
        openDoorVo.setOrderNo(orderNo);
        openDoorVo.setCab(String.valueOf(cab));
        openDoorVo.setSn("1");
        openDoorVo.setCol(String.valueOf(col));
        openDoorVo.setPrice("0.00");
        openDoorVo.setNo(MessageFormat.format("{0}_{1}", deliveryLocker.getLockerLabel(), col));
        openDoorVo.setName("");
        openDoorVo.setType("5");

        deliveryLockerOpenApiService.openDoorWithoutSleep(clientNo, orderNo, appId, appKey, openDoorVo);
        return ResultGenerator.genSuccessResult();
    }


    /**
     * 2.6.9 通过订单 ID 打开该订单所分配的外卖柜
     *
     * @param orderId 订单 ID
     */
    @TokenRequired(role = TokenRequired.ROLE_TENANT)
    @GetMapping("/openDeliveryLockerBoxByOrderId")
    public Result<Void> openDeliveryLockerBoxByOrderId(@RequestParam(value = "orderId") String orderId,
                                                       @RequestParam(value = "waitIntervalInMs", defaultValue = "3000") Integer waitIntervalInMs) {
        log.info("/openDeliveryLockerBoxByOrderId, orderId: {}, waitIntervalInMs: {}", orderId, waitIntervalInMs);
        LambdaQueryWrapper<OrderDetail> orderDetailWrapper = new LambdaQueryWrapper<OrderDetail>()
                .eq(OrderDetail::getOrderId, orderId)
                .select(OrderDetail::getId, OrderDetail::getLockerInfo, OrderDetail::getLockerBoxNo)
                .orderByAsc(OrderDetail::getLockerBoxNo);
        List<OrderDetail> orderDetailList = orderDetailMapper.selectList(orderDetailWrapper);
        if (CollectionUtils.isEmpty(orderDetailList)) {
            log.warn("/openDeliveryLockerBoxByOrderId, 未找到对应的订单详情, orderId: {}", orderId);
            return ResultGenerator.genFailResult("ID 为 " + orderId + " 的订单详情不存在");
        }

        List<OrderDetail> needToOpenDoorList = orderDetailList.stream()
                .filter(orderDetail -> StringUtils.hasText(orderDetail.getLockerInfo()))
                .collect(Collectors.toList());

        log.info("/openDeliveryLockerBoxByOrderId, 有 {} 个外卖柜窗口需要打开", needToOpenDoorList.size());
        List<DeliveryLockerOpenDoorVO> openDoorArgsList = new ArrayList<>(needToOpenDoorList.size());
        for (OrderDetail orderDetail : needToOpenDoorList) {
            DeliverBoxApiParamVO boxApiParamVO = JSONObject.parseObject(orderDetail.getLockerInfo(), DeliverBoxApiParamVO.class);

            DeliveryLockerOpenDoorParameterVO openDoorVo = new DeliveryLockerOpenDoorParameterVO();
            // 这里直接使用订单详情 ID 作为 orderNo，每笔订单详情 ID 一定是不重复的
            String orderNo = orderDetail.getId();
            openDoorVo.setOrderNo(orderNo);
            Integer cab = boxApiParamVO.getCab();
            openDoorVo.setCab(String.valueOf(cab));
            // TODO: 验证 sn 一直为 1 是否可以正常批量打开柜门
            openDoorVo.setSn("1");
            Integer col = boxApiParamVO.getCol();
            openDoorVo.setCol(String.valueOf(col));
            openDoorVo.setPrice("0.00");
            openDoorVo.setNo(orderDetail.getLockerBoxNo());
            openDoorVo.setName("");
            openDoorVo.setType("5");

            DeliveryLockerOpenDoorVO openDoorArgs = new DeliveryLockerOpenDoorVO();
            openDoorArgs.setClientNo(boxApiParamVO.getClientNo());
            openDoorArgs.setAppId(boxApiParamVO.getAppId());
            openDoorArgs.setAppKey(boxApiParamVO.getAppKey());
            openDoorArgs.setOrderNo(orderNo);
            openDoorArgs.setParameter(openDoorVo);

            openDoorArgsList.add(openDoorArgs);
        }

        // 根据 clientNo + appId + appKey 分组，以便于连接相同外卖柜控制终端的外卖柜窗口，可以调用批量开柜门接口，减少调用接口次数和等待时间
        Map<String, List<DeliveryLockerOpenDoorVO>> openDoorArgsGroupByClient = openDoorArgsList.stream()
                .collect(Collectors.groupingBy(args -> args.getClientNo() + args.getAppId() + args.getAppKey()));
        int callOpenDoorApiCount = 0;
        for (List<DeliveryLockerOpenDoorVO> openDoorArgsListInGroup : openDoorArgsGroupByClient.values()) {
            DeliveryLockerOpenDoorVO commonArgs = openDoorArgsListInGroup.get(0);
            String clientNo = commonArgs.getClientNo();
            String appId = commonArgs.getAppId();
            String appKey = commonArgs.getAppKey();
            List<DeliveryLockerOpenDoorParameterVO> parameter = openDoorArgsListInGroup.stream()
                    .map(DeliveryLockerOpenDoorVO::getParameter)
                    .collect(Collectors.toList());

            // 调用批量打开柜门接口
            log.info("/openDeliveryLockerBoxByOrderId, 对于 clientNo: {}, appId: {}, appKey: {} 的外卖柜终端，有 {} 个窗口需要打开", clientNo, appId, appKey, parameter.size());
            deliveryLockerOpenApiService.openDoorListMoreThanFive(clientNo, appId, appKey, parameter, waitIntervalInMs);
            callOpenDoorApiCount++;
        }
        log.info("/openDeliveryLockerBoxByOrderId, 调用打开柜门接口 {} 次", callOpenDoorApiCount);
        return ResultGenerator.genSuccessResult();
    }


    /**
     * 2.6.10 查询批量打开柜门时默认的等待间隔时间
     *
     * @param companyId 公司 ID
     */
    @TokenRequired(role = TokenRequired.ROLE_TENANT)
    @GetMapping("/queryWaitIntervalForBatchOpenMultiDoor")
    public Result<List<WaitIntervalForBatchOpenMultiDoorRespVo>> queryWaitIntervalForBatchOpenMultiDoor(@RequestParam(value = "companyId", required = false, defaultValue = "00000000000000000000000000000000") String companyId) {
        log.info("/queryWaitIntervalForBatchOpenMultiDoor, companyId: {}", companyId);
        LambdaQueryWrapper<DeliveryLocker> deliveryLockerQueryWrapper = new LambdaQueryWrapper<DeliveryLocker>()
                .eq(DeliveryLocker::getCompanyId, companyId)
                .eq(DeliveryLocker::getDelFlag, false)
                // 根据调用开柜门接口的等待间隔时间降序排序
                .orderByDesc(DeliveryLocker::getWaitInterval);
        List<DeliveryLocker> deliveryLockerList = deliverLockerMapper.selectList(deliveryLockerQueryWrapper);
        int waitInternalInMs = defaultSelectedWaitIntervalInMs;
        if (!CollectionUtils.isEmpty(deliveryLockerList)) {
            // 由于返回的外卖柜信息按照等待间隔时间降序排列，所以返回最长的等待间隔时间时，返回第一个元素的等待间隔时间即可
            waitInternalInMs = deliveryLockerList.get(0).getWaitInterval();
        }

        // 根据配置项构建默认的等待间隔时间下拉列表
        List<WaitIntervalForBatchOpenMultiDoorRespVo> voList = Arrays.stream(waitIntervalInMsList.split(","))
                .map(Integer::valueOf)
                .map(waitInterval -> new WaitIntervalForBatchOpenMultiDoorRespVo(waitInterval, false))
                .collect(Collectors.toList());

        // 设置选中项
        for (WaitIntervalForBatchOpenMultiDoorRespVo vo : voList) {
            if (vo.getWaitIntervalInMs().equals(waitInternalInMs)) {
                vo.setSelected(true);
            }
        }

        log.info("/queryWaitIntervalForBatchOpenMultiDoor, response: {}", JSONObject.toJSONString(voList));
        return ResultGenerator.genSuccessResult(voList);
    }
}
